import sys
import os
from pathlib import Path

# 将项目根目录添加到 sys.path
# e.g., E:\Recommendation
# backend/app/api/recommend.py -> backend/app/api -> backend/app -> backend -> root
ROOT_DIR = Path(__file__).resolve().parent.parent.parent.parent
sys.path.append(str(ROOT_DIR))

from fastapi import APIRouter, Depends, HTTPException, Query, Request
import math
from fastapi.responses import StreamingResponse
import pandas as pd
import json
import re
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.db import SessionLocal
from app.models.user import User
from app.models.movie import Movie
from app.models.rating import Rating
from jose import jwt
from pydantic import BaseModel

from src.recommendation.hybrid_recommender import HybridRecommender
from src.recommendation.cold_start import ColdStartRecommender
from src.recommendation.content_recommender import ContentRecommender

def _find_latest_model_dir(models_root: Path) -> Path | None:
    """在 models 根目录下查找最新的混合模型目录（含 saved_model.pb）。
    优先依据目录名中的时间戳 `hybrid_model_YYYYMMDD_HHMMSS`，否则按修改时间降序。
    """
    try:
        if not models_root.exists():
            return None
        candidates: list[tuple[Path, float]] = []
        for p in models_root.iterdir():
            try:
                if not p.is_dir():
                    continue
                name = p.name
                if not name.startswith('hybrid_model_'):
                    continue
                if not (p / 'saved_model.pb').exists():
                    continue
                # 尝试从名称解析时间戳
                ts_score = None
                try:
                    from datetime import datetime
                    ts = name.replace('hybrid_model_', '')  # YYYYMMDD_HHMMSS
                    dt = datetime.strptime(ts, '%Y%m%d_%H%M%S')
                    ts_score = dt.timestamp()
                except Exception:
                    ts_score = None
                if ts_score is None:
                    ts_score = (p / 'saved_model.pb').stat().st_mtime
                candidates.append((p, float(ts_score)))
            except Exception:
                continue
        if not candidates:
            return None
        candidates.sort(key=lambda x: x[1], reverse=True)
        return candidates[0][0]
    except Exception:
        return None

# --- 全局推荐器实例 ---
# 使用 pathlib 确保路径在不同操作系统下的兼容性
MODELS_ROOT = ROOT_DIR / "models"
LATEST_MODEL_DIR = _find_latest_model_dir(MODELS_ROOT)
MODEL_PATH = LATEST_MODEL_DIR if LATEST_MODEL_DIR else (ROOT_DIR / "models" / "hybrid_model_20250925_134529")
DATA_PATH = ROOT_DIR / "data" / "processed"
TMDB_JSON_DIR = ROOT_DIR / "data" / "tmdb" / "json"
VIDEO_DIR = ROOT_DIR / "data" / "videos"

recommender = None
cold_start_recommender = None
content_recommender: ContentRecommender | None = None
_movie_to_tmdb: dict[int, int] | None = None
_tmdb_to_movie: dict[int, int] | None = None
_movies_meta_df: pd.DataFrame | None = None
_movie_lang_cache: dict[int, str] = {}

try:
    if MODEL_PATH and Path(MODEL_PATH).exists():
        print(f"🚀 加载混合推荐模型: {MODEL_PATH}")
        recommender = HybridRecommender(str(MODEL_PATH), str(DATA_PATH))
    else:
        print(f"⚠️ 混合模型路径不存在: {MODEL_PATH}")

    # 先尝试加载内容相似推荐器（占用内存较大，成功后可复用给冷启动）
    movie_features_path = DATA_PATH / "movie_features.pkl"
    movies_meta_path = DATA_PATH / "movies_cleaned.pkl"
    if movie_features_path.exists() and movies_meta_path.exists():
        try:
            content_recommender = ContentRecommender(str(movie_features_path), str(movies_meta_path))
            print("✅ 内容相似推荐器已就绪")
        except Exception as e:
            print(f"⚠️ 内容相似推荐器加载失败: {e}")

    print(f"🚀 加载冷启动推荐器数据: {DATA_PATH}")
    cold_start_recommender = ColdStartRecommender(str(DATA_PATH), content_recommender=content_recommender, enable_content=(content_recommender is None))
    print("✅ 推荐器加载完成")

    # 加载 movieId -> tmdbId 映射 和 电影基础信息（年份、标题等）
    links_path = DATA_PATH / "links_cleaned.pkl"
    movies_meta_path = DATA_PATH / "movies_cleaned.pkl"
    if links_path.exists():
        try:
            links_df = pd.read_pickle(links_path)
            if 'movieId' in links_df.columns and 'tmdbId' in links_df.columns:
                df = links_df[['movieId', 'tmdbId']].copy()
                # 将非数值转换为 NaN，并移除无效值
                df['movieId'] = pd.to_numeric(df['movieId'], errors='coerce')
                df['tmdbId'] = pd.to_numeric(df['tmdbId'], errors='coerce')
                df = df.dropna(subset=['movieId', 'tmdbId'])
                df = df[df['tmdbId'] > 0]
                _movie_to_tmdb = dict(zip(df['movieId'].astype(int), df['tmdbId'].astype(int)))
                # 反向映射：tmdbId -> movieId，便于 API 兼容传入 tmdbId 的情况
                try:
                    _tmdb_to_movie = { int(t): int(m) for m, t in _movie_to_tmdb.items() }
                except Exception:
                    _tmdb_to_movie = None
                print(f"✅ TMDB 映射加载完成，共 {len(_movie_to_tmdb)} 条")
            else:
                print("⚠️ links_cleaned.pkl 缺少必要列: movieId/tmdbId")
        except Exception as e:
            print(f"⚠️ TMDB 映射加载警告: {e}")
    if movies_meta_path.exists():
        try:
            _movies_meta_df = pd.read_pickle(movies_meta_path)
        except Exception as e:
            print(f"⚠️ movies_cleaned.pkl 加载警告: {e}")

except Exception as e:
    print(f"❌ 加载推荐器时发生严重错误: {e}")
    # 如果冷启动推荐器也失败，则设置为 None
    if cold_start_recommender is None:
        cold_start_recommender = None
    recommender = None


router = APIRouter()
SECRET_KEY = "your_secret_key"
ALGORITHM = "HS256"

class RateRequest(BaseModel):
    movie_id: int
    score: float

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


def _json_safe(obj):
    """递归清洗对象中的 NaN/Inf，转换为 0 或 None 以通过 JSON 标准。
    - 对 float: NaN/Inf -> 0.0
    - 对 list/dict: 递归处理
    其他类型原样返回。
    """
    try:
        if isinstance(obj, float):
            if math.isnan(obj) or math.isinf(obj):
                return 0.0
            return obj
        if isinstance(obj, (list, tuple)):
            return [ _json_safe(x) for x in obj ]
        if isinstance(obj, dict):
            return { k: _json_safe(v) for k, v in obj.items() }
        return obj
    except Exception:
        return obj

def get_current_user(token: str, db: Session):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user = db.query(User).filter(User.id == payload["user_id"]).first()
        if not user:
            raise HTTPException(status_code=401, detail="Invalid user")
        return user
    except Exception:
        raise HTTPException(status_code=401, detail="Invalid token")


def _load_tmdb_json(tmdb_id: int) -> dict | None:
    """从本地 TMDB JSON 目录读取详情。"""
    try:
        json_path = TMDB_JSON_DIR / f"{tmdb_id}.json"
        if json_path.exists():
            with open(json_path, 'r', encoding='utf-8') as f:
                return json.load(f)
    except Exception:
        return None
    return None


def _enrich_with_tmdb(recs: list[dict]) -> list[dict]:
    """为推荐结果补充 TMDB 的 poster、backdrop、overview、genres、year 等字段。"""
    if not recs:
        return recs
    enriched = []
    for r in recs:
        item = dict(r)
        movie_id = item.get('movieId') or item.get('id')
        if movie_id is None:
            enriched.append(item)
            continue
        tmdb_id = _movie_to_tmdb.get(int(movie_id)) if _movie_to_tmdb else None
        if tmdb_id:
            data = _load_tmdb_json(tmdb_id)
            if data:
                poster_path = data.get('poster_path')
                backdrop_path = data.get('backdrop_path')
                overview = data.get('overview')
                title = data.get('title') or data.get('name')
                release_date = data.get('release_date') or data.get('first_air_date')
                year = item.get('year')
                if release_date and (not year or year == '未知'):
                    year = release_date.split('-')[0]
                # genres 兼容多种格式（list[dict]|list[str]|str）
                genres = item.get('genres')
                if not genres:
                    dg = data.get('genres')
                    genres_list: list[str] = []
                    if isinstance(dg, list) and dg and isinstance(dg[0], dict):
                        genres_list = [g.get('name') for g in dg if isinstance(g, dict) and g.get('name')]
                    elif isinstance(dg, list) and dg and isinstance(dg[0], str):
                        genres_list = [g for g in dg if isinstance(g, str)]
                    elif isinstance(dg, str):
                        genres_list = [s.strip() for s in dg.split(',') if s.strip()]
                    genres = '|'.join([g for g in genres_list if g]) if genres_list else None

                # 演职员：actors/directors 兼容 list[str] 或 str
                actors = data.get('actors')
                if isinstance(actors, str):
                    actors = [a.strip() for a in actors.split(',') if a.strip()]
                actors = [a for a in actors or [] if isinstance(a, str)]
                directors = data.get('directors')
                if isinstance(directors, str):
                    directors = [d.strip() for d in directors.split(',') if d.strip()]
                directors = [d for d in directors or [] if isinstance(d, str)]
                imdb_id = data.get('imdb_id') or data.get('imdbId')
                # 组合 TMDB 图片完整 URL（如需离线可改为本地缓存）
                base_img = "https://image.tmdb.org/t/p"
                item.update({
                    'tmdbId': tmdb_id,
                    'poster': f"{base_img}/w500{poster_path}" if poster_path else item.get('poster'),
                    # 使用更高分辨率的横图，确保前端大横幅清晰
                    'backdrop': f"{base_img}/w1280{backdrop_path}" if backdrop_path else item.get('backdrop'),
                    'overview': overview or item.get('overview'),
                    'title': title or item.get('title'),
                    'year': year or item.get('year'),
                    'genres': genres or item.get('genres'),
                    'actors': actors or item.get('actors'),
                    'directors': directors or item.get('directors'),
                    'imdbId': imdb_id or item.get('imdbId'),
                    'original_language': data.get('original_language') or item.get('original_language'),
                    'vote_average': data.get('vote_average') or item.get('vote_average'),
                    'vote_count': data.get('vote_count') or item.get('vote_count'),
                })
        # 兜底：如果没有 TMDB，尽量从 movies_cleaned 中补 year/genres/title
        if _movies_meta_df is not None and ('year' not in item or 'genres' not in item or 'title' not in item):
            try:
                row = _movies_meta_df[_movies_meta_df['movieId'] == int(movie_id)]
                if not row.empty:
                    row0 = row.iloc[0]
                    item.setdefault('title', row0.get('title'))
                    item.setdefault('genres', row0.get('genres'))
                    item.setdefault('year', row0.get('year'))
            except Exception:
                pass
        enriched.append(item)
    return enriched


def _get_movie_language(movie_id: int) -> str | None:
    """获取电影的原始语言，使用缓存，来源于 TMDB JSON。"""
    try:
        mid = int(movie_id)
    except Exception:
        return None
    if mid in _movie_lang_cache:
        return _movie_lang_cache.get(mid)
    tmdb_id = _movie_to_tmdb.get(mid) if _movie_to_tmdb else None
    if not tmdb_id:
        _movie_lang_cache[mid] = None
        return None
    data = _load_tmdb_json(int(tmdb_id))
    lang = None
    if isinstance(data, dict):
        lang = data.get('original_language')
    _movie_lang_cache[mid] = lang
    return lang


def _normalize_movie_id(mid: int) -> int:
    """将传入的 ID 规范化为 MovieLens 的 movieId。
    - 若 mid 本身是 movieId（存在于 _movie_to_tmdb 的键），直接返回。
    - 若 mid 是 tmdbId（存在于反向映射中），转换为 movieId。
    - 其他情况原样返回。
    """
    try:
        m = int(mid)
    except Exception:
        return mid
    try:
        if _movie_to_tmdb and m in _movie_to_tmdb:
            return m
        if _tmdb_to_movie and m in _tmdb_to_movie:
            return _tmdb_to_movie[m]
    except Exception:
        pass
    return m


def _personalized_content_fallback(user_id: int, top_k: int = 10) -> list[dict]:
    """基于用户历史评分 + 内容相似的个性化兜底。
    步骤：
    1) 读取该用户的历史评分（优先数据库，补充 processed 数据集）。
    2) 选取若干高分影片作为种子。
    3) 对每个种子调用内容相似推荐，按相似度 * 用户对种子电影的评分权重聚合。
    4) 过滤用户看过的影片和种子本身，返回 top_k。
    """
    try:
        if content_recommender is None:
            return []
        # 读取历史评分明细（尽量多拿一些，保证多样性）
        seeds_src = _fallback_user_ratings_from_processed(int(user_id), limit=800)
        if not seeds_src:
            return []
        # 收集已看过，便于过滤
        seen_ids = {int(it['movieId']) for it in seeds_src if it.get('movieId') is not None}
        # 根据分数挑选前若干种子
        seeds_sorted = sorted(
            [it for it in seeds_src if it.get('movieId') is not None],
            key=lambda x: float(x.get('score') or 0.0), reverse=True
        )
        # 高分优先：>=4.0 的最多取 8 个；不够再补至最多 8 个
        high = [it for it in seeds_sorted if float(it.get('score') or 0.0) >= 4.0][:8]
        if len(high) < 4:
            extra = [it for it in seeds_sorted if it not in high][: (8 - len(high))]
            seeds = high + extra
        else:
            seeds = high
        if not seeds:
            return []
        # 每个种子请求的相似条数，合计不超过 ~ top_k*4，足够聚合
        per_seed = max(10, min(30, top_k * 3))
        agg: dict[int, float] = {}
        for it in seeds:
            try:
                mid = int(it['movieId'])
                score = float(it.get('score') or 0.0)
            except Exception:
                continue
            # 评分权重 0..1（最多按 5 分归一化），略微平滑
            w = max(0.0, min(1.0, (score - 2.5) / 2.5))  # 2.5 以下权重趋近 0
            if w <= 0:
                continue
            try:
                sims = content_recommender.recommend_similar_to(mid, top_k=per_seed)
            except Exception:
                continue
            for s in sims or []:
                try:
                    cid = int(s.get('movieId') or s.get('id'))
                except Exception:
                    continue
                if cid in seen_ids or cid == mid:
                    continue
                sim = s.get('similarity')
                try:
                    sim = float(sim)
                except Exception:
                    sim = 0.0
                if not math.isfinite(sim):
                    sim = 0.0
                agg[cid] = agg.get(cid, 0.0) + sim * (0.5 + 0.5 * w)  # 提升高分种子的贡献
        if not agg:
            return []
        # 取前 top_k，并构造基础结构用于 enrich
        top_items = sorted(agg.items(), key=lambda kv: kv[1], reverse=True)[: top_k]
        basics = [{'movieId': mid, 'id': mid, 'predicted_rating': round(float(score), 4)} for mid, score in top_items]
        return _enrich_with_tmdb(basics)
    except Exception:
        return []


@router.get("/catalog")
def get_movie_catalog(
    page: int = Query(1, ge=1),
    page_size: int = Query(28, ge=1, le=100),
    genre: str | None = Query(None, description="按类型精确匹配（genres 中的某一项）"),
    year_range: str | None = Query(None, description="2020s|2010s|2000s|1990s|older"),
    lang: str | None = Query(None, description="TMDB 原始语言代码，如 en/zh/ja"),
    sort: str = Query('hot', description="hot|score|year"),
    q: str | None = Query(None, description="标题模糊搜索（不区分大小写，包含关系）"),
):
    """返回“全部电影”分页列表，支持类型/年份/语言/排序过滤。
    数据来源：processed/movies_cleaned.pkl + TMDB 本地 JSON enrich。
    """
    if _movies_meta_df is None or _movies_meta_df.empty:
        raise HTTPException(status_code=503, detail="电影元数据不可用")

    # 仅保留必要列，容错不同列名
    df = _movies_meta_df.copy()
    # 标准化列
    if 'movieId' not in df.columns:
        raise HTTPException(status_code=503, detail="电影元数据缺少 movieId 列")
    if 'title' not in df.columns:
        df['title'] = None
    if 'genres' not in df.columns:
        df['genres'] = None
    if 'year' not in df.columns:
        df['year'] = None

    # 预处理年份
    df['year_num'] = pd.to_numeric(df['year'], errors='coerce')

    # 过滤：关键词（标题模糊）
    if q:
        try:
            qs = str(q).strip()
            if qs:
                df = df[df['title'].astype(str).str.contains(re.escape(qs), case=False, na=False)]
        except Exception:
            pass

    # 过滤：类型
    if genre:
        try:
            # 匹配 | 分隔的精确项
            pattern = rf'(?:^|\|){re.escape(genre)}(?:\||$)'
            df = df[df['genres'].astype(str).str.contains(pattern, regex=True, na=False)]
        except Exception:
            pass

    # 过滤：年份段
    if year_range:
        if year_range == '2020s':
            df = df[df['year_num'] >= 2020]
        elif year_range == '2010s':
            df = df[(df['year_num'] >= 2010) & (df['year_num'] <= 2019)]
        elif year_range == '2000s':
            df = df[(df['year_num'] >= 2000) & (df['year_num'] <= 2009)]
        elif year_range == '1990s':
            df = df[(df['year_num'] >= 1990) & (df['year_num'] <= 1999)]
        elif year_range == 'older':
            df = df[df['year_num'] < 1990]

    # 过滤：语言（需要 TMDB）
    if lang:
        try:
            mids = df['movieId'].astype(int).tolist()
            keep = []
            for mid in mids:
                lg = _get_movie_language(mid)
                if lg and str(lg).strip() == lang:
                    keep.append(mid)
            df = df[df['movieId'].isin(keep)]
        except Exception:
            # 语言过滤失败时，不致命，忽略过滤
            pass

    # 排序优先级
    # 若 df 有统计列（rating_count/rating_mean），则参与排序；否则回退到 year/title
    has_rc = 'rating_count' in df.columns
    has_rm = 'rating_mean' in df.columns
    if sort == 'score' and has_rm:
        by = [('rating_mean', False)]
        if has_rc:
            by.append(('rating_count', False))
        by.append(('year_num', False))
    elif sort == 'year':
        by = [('year_num', False)]
        if has_rm:
            by.append(('rating_mean', False))
        if has_rc:
            by.append(('rating_count', False))
    else:  # hot
        by = []
        if has_rc:
            by.append(('rating_count', False))
        if has_rm:
            by.append(('rating_mean', False))
        by.append(('year_num', False))
    if by:
        try:
            df = df.sort_values(by=[c for c, _ in by], ascending=[asc for _, asc in [(c, a) for c, a in by]])
        except Exception:
            # 如果排序出错，忽略排序
            pass

    total = int(len(df))
    # 分页
    start = max(0, (page - 1) * page_size)
    end = start + page_size
    page_df = df.iloc[start:end][['movieId', 'title', 'genres', 'year']].copy()

    # 构造基本结构并 enrich
    items = []
    for _, r in page_df.iterrows():
        base = {
            'movieId': int(r.get('movieId')) if pd.notna(r.get('movieId')) else None,
            'id': int(r.get('movieId')) if pd.notna(r.get('movieId')) else None,
            'title': r.get('title'),
            'genres': r.get('genres'),
            'year': r.get('year'),
        }
        items.append(base)
    items = _enrich_with_tmdb(items)

    # 统一字段输出
    out = []
    for it in items:
        out.append({
            'id': it.get('movieId') or it.get('id'),
            'movieId': it.get('movieId') or it.get('id'),
            'title': it.get('title'),
            'genres': it.get('genres'),
            'year': it.get('year'),
            'poster': it.get('poster'),
            'backdrop': it.get('backdrop'),
            'overview': it.get('overview'),
            'original_language': it.get('original_language'),
            'rating_mean': it.get('rating_mean') if 'rating_mean' in it else None,
            'rating_count': it.get('rating_count') if 'rating_count' in it else None,
            'vote_average': it.get('vote_average'),
            'vote_count': it.get('vote_count'),
        })

    return _json_safe({
        'total': total,
        'page': page,
        'page_size': page_size,
        'items': out,
    })


@router.get("/catalog/options")
def get_catalog_options():
    """返回筛选可用选项，目前提供所有类型列表及年份范围。语言选项依赖 TMDB，暂不全量扫描。"""
    if _movies_meta_df is None or _movies_meta_df.empty:
        raise HTTPException(status_code=503, detail="电影元数据不可用")
    df = _movies_meta_df
    genres_set = set()
    try:
        for g in df.get('genres', pd.Series([], dtype=str)).astype(str).dropna().tolist():
            if not g:
                continue
            for s in str(g).split('|'):
                s = s.strip()
                if s:
                    genres_set.add(s)
    except Exception:
        pass
    # 年份范围
    years = pd.to_numeric(df.get('year', pd.Series([], dtype=float)), errors='coerce')
    years = years.dropna().astype(int)
    year_min = int(years.min()) if not years.empty else None
    year_max = int(years.max()) if not years.empty else None
    return _json_safe({
        'genres': sorted(list(genres_set)),
        'year_min': year_min,
        'year_max': year_max,
    })


def _fallback_ratings_items_from_processed(movie_id: int, limit: int = 10) -> list[dict]:
    """当应用数据库没有评分时，从 processed 数据集中抽取该片的评分明细。
    输出 items: [{"username": "用户<id>", "score": float}]
    """
    try:
        # 优先 feather（列裁剪），不存在则回退 pkl
        df: pd.DataFrame | None = None
        feather_path = DATA_PATH / 'ratings_cleaned.feather'
        pkl_path = DATA_PATH / 'ratings_cleaned.pkl'
        if feather_path.exists():
            try:
                df = pd.read_feather(feather_path, columns=['userId', 'movieId', 'rating'])
            except Exception:
                # 某些环境不支持按列读取，退而全读
                df = pd.read_feather(feather_path)
        elif pkl_path.exists():
            df = pd.read_pickle(pkl_path)
        if df is None or df.empty:
            return []
        # 兼容列名
        uid_col = 'userId' if 'userId' in df.columns else 'user_id'
        mid_col = 'movieId' if 'movieId' in df.columns else 'movie_id'
        rating_col = 'rating' if 'rating' in df.columns else ('score' if 'score' in df.columns else None)
        if rating_col is None or uid_col not in df.columns or mid_col not in df.columns:
            return []
        # 过滤到目标电影
        try:
            df[mid_col] = pd.to_numeric(df[mid_col], errors='coerce')
        except Exception:
            pass
        sub = df[df[mid_col] == int(movie_id)][[uid_col, rating_col]]
        if sub.empty:
            return []
        # 随机抽取 limit 条
        n = min(limit, len(sub))
        try:
            picked = sub.sample(n=n, random_state=None)
        except Exception:
            picked = sub.head(n)
        items: list[dict] = []
        for _, row in picked.iterrows():
            try:
                uid = int(row[uid_col]) if pd.notna(row[uid_col]) else 0
            except Exception:
                uid = 0
            try:
                sc = float(row[rating_col]) if pd.notna(row[rating_col]) else 0.0
            except Exception:
                sc = 0.0
            items.append({
                "username": f"用户{uid if uid > 0 else ''}".rstrip(),
                "score": sc
            })
        return items
    except Exception:
        return []


def _range_stream(file_path: Path, start: int, end: int, chunk_size: int = 1024 * 1024):
    with open(file_path, 'rb') as f:
        f.seek(start)
        remaining = end - start + 1
        while remaining > 0:
            read_size = min(chunk_size, remaining)
            data = f.read(read_size)
            if not data:
                break
            remaining -= len(data)
            yield data

@router.get("/movies")
def recommend_movies(
    token: str = Query(None), 
    top_k: int = Query(10, description="推荐数量"),
    db: Session = Depends(get_db)
):
    """
    获取电影推荐列表。
    - 如果提供了有效的 token，返回个性化推荐。
    - 否则，返回热门电影（冷启动）。
    """
    # 场景1: 有 token，尝试个性化推荐
    if token:
        try:
            user = get_current_user(token, db)
            if recommender:
                print(f"👤 为用户 {user.id} 生成个性化推荐...")
                # 调用混合模型进行推荐
                recommendations = recommender.recommend(user.id, top_k=top_k, diversity=True)
                # 若个性化为空，先尝试基于历史评分的内容相似兜底，再降级热门
                if not recommendations:
                    print("ℹ️ 个性化结果为空，尝试内容相似个性化兜底")
                    per_fallback = _personalized_content_fallback(int(user.id), top_k=top_k)
                    if per_fallback:
                        return _json_safe(per_fallback)
                    print("ℹ️ 内容相似兜底为空，降级为热门推荐")
                    if cold_start_recommender:
                        recommendations = cold_start_recommender.recommend_popular(top_k=top_k)
                return _json_safe(_enrich_with_tmdb(recommendations))
            else:
                print(f"⚠️ 混合推荐器不可用，为用户 {user.id} 提供冷启动推荐。")
                # 即使混合不可用，也先尝试个性化内容兜底
                per_fallback = _personalized_content_fallback(int(user.id), top_k=top_k)
                if per_fallback:
                    return _json_safe(per_fallback)
        except HTTPException as e:
            # token 无效或用户不存在，降级为冷启动
            print(f"🚨 Token 无效或用户查找失败 ({e.detail})，降级为冷启动推荐。")
            pass

    # 场景2: 无 token 或个性化推荐失败，执行冷启动策略
    if cold_start_recommender:
        print("🥶 执行冷启动推荐...")
        recommendations = cold_start_recommender.recommend_popular(top_k=top_k)
        return _json_safe(_enrich_with_tmdb(recommendations))
    else:
        # 极端情况：所有推荐器都不可用
        print("❌ 所有推荐器均不可用！")
        raise HTTPException(
            status_code=503, 
            detail="推荐服务当前不可用，请稍后再试。"
        )

@router.post("/rate")
async def rate_movie(token: str | None = Query(None), req: RateRequest | None = None, request: Request = None, db: Session = Depends(get_db)):
    # 兼容多种 token 传递方式：query 参数、Authorization Bearer、请求体字段
    if token is None:
        # Authorization: Bearer <token>
        try:
            auth = request.headers.get('Authorization') or request.headers.get('authorization')
            if auth and auth.lower().startswith('bearer '):
                token = auth.split(' ', 1)[1].strip()
        except Exception:
            pass
    if token is None:
        try:
            body = await request.json()
            if isinstance(body, dict) and 'token' in body:
                token = body.get('token')
            # 若未通过 Pydantic 解析，手动构建 req
            if req is None and isinstance(body, dict):
                try:
                    req = RateRequest(movie_id=int(body.get('movie_id')), score=float(body.get('score')))
                except Exception:
                    pass
        except Exception:
            pass

    if req is None:
        raise HTTPException(status_code=400, detail="Invalid request body")

    user = get_current_user(token, db)
    # 检查是否已评分
    exist = db.query(Rating).filter(Rating.user_id == user.id, Rating.movie_id == req.movie_id).first()
    if exist:
        # 支持修改评分
        exist.score = req.score
        db.add(exist)
        db.commit()
        return {"msg": "rating updated"}
    rating = Rating(user_id=user.id, movie_id=req.movie_id, score=req.score)
    db.add(rating)
    db.commit()
    return {"msg": "rating success"}

@router.get("/movie/{movie_id}")
def get_movie_detail(movie_id: int, db: Session = Depends(get_db)):
    """返回富详情：包含海报、年份、类型、简介等（合并 TMDB 与本地元数据）。
    若数据库中找不到该电影，则回退到 processed/movies_cleaned.pkl 与 TMDB JSON。
    """
    movie = db.query(Movie).filter(Movie.id == movie_id).first()

    # 基础信息：尽量包含 movieId，便于 TMDB enrich
    base: dict = {"id": int(movie_id), "movieId": int(movie_id)}
    if movie:
        base.update({"title": movie.title})
    else:
        # 回退读取本地元数据
        try:
            if _movies_meta_df is not None:
                row = _movies_meta_df[_movies_meta_df['movieId'] == int(movie_id)]
                if not row.empty:
                    r0 = row.iloc[0]
                    base.update({
                        "title": r0.get('title'),
                        "year": r0.get('year'),
                        "genres": r0.get('genres'),
                    })
        except Exception:
            pass

    enriched = _enrich_with_tmdb([base])
    detail = enriched[0] if enriched else base

    # 直接读取 TMDB 原始 JSON，确保透传字段存在（避免 enrich 阶段未复制导致缺失）
    tmdb_id_for_detail = detail.get('tmdbId')
    if not tmdb_id_for_detail and _movie_to_tmdb:
        try:
            tmdb_id_for_detail = _movie_to_tmdb.get(int(movie_id))
        except Exception:
            tmdb_id_for_detail = None
    tmdb_raw: dict | None = _load_tmdb_json(int(tmdb_id_for_detail)) if tmdb_id_for_detail else None
    # 统一字段命名，便于前端使用
    # 汇总标准输出（不包含 imdbId 按用户要求隐藏）
    out = {
        "id": detail.get("id") or detail.get("movieId"),
        "title": detail.get("title"),
        "year": detail.get("year"),
        "genres": detail.get("genres"),
        "overview": detail.get("overview"),
        "poster": detail.get("poster"),
        "backdrop": detail.get("backdrop"),
        "tmdbId": detail.get("tmdbId"),
        "actors": detail.get("actors") or [],
        "directors": detail.get("directors") or [],
    }
    # 透传更多详情信息（如果 TMDB JSON 有）
    passthrough_keys = [
        'vote_average','vote_count','status','revenue','runtime','adult','budget','homepage',
        'original_language','original_title','tagline','production_companies','production_countries',
        'spoken_languages','keywords','popularity'
    ]
    for k in passthrough_keys:
        # 优先使用 TMDB 原始 JSON；若不存在再回退 enrich 结果
        val = None
        if isinstance(tmdb_raw, dict) and k in tmdb_raw and tmdb_raw.get(k) is not None:
            val = tmdb_raw.get(k)
        elif k in detail and detail.get(k) is not None:
            val = detail.get(k)
        if val is not None:
            out[k] = val
    return _json_safe(out)


@router.get("/movie/{movie_id}/similar")
def get_similar_movies(movie_id: int, top_k: int = Query(10, ge=1, le=50)):
    """基于内容特征返回与指定电影最相似的影片列表。"""
    if content_recommender is None:
        raise HTTPException(status_code=503, detail="相似推荐服务未启用")
    try:
        recs = content_recommender.recommend_similar_to(movie_id, top_k=top_k)
        recs = _enrich_with_tmdb(recs)
        # 统一输出字段
        out = []
        for r in recs:
            sim = r.get("similarity")
            try:
                sim = float(sim)
                if math.isnan(sim) or math.isinf(sim):
                    sim = 0.0
            except Exception:
                sim = 0.0
            out.append({
                "id": r.get("movieId") or r.get("id"),
                "title": r.get("title"),
                "similarity": round(sim, 6),
                "poster": r.get("poster"),
                "year": r.get("year"),
                "genres": r.get("genres"),
            })
        return _json_safe(out)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"相似推荐失败: {e}")

def _fallback_user_ratings_from_processed(user_id: int, limit: int = 500) -> list[dict]:
    """当应用数据库没有该用户评分时，从 processed 数据集中抽取其评分。
    输出 items: [{ movieId, score }]
    """
    try:
        df: pd.DataFrame | None = None
        feather_path = DATA_PATH / 'ratings_cleaned.feather'
        pkl_path = DATA_PATH / 'ratings_cleaned.pkl'
        raw_csv_path = ROOT_DIR / 'data' / 'raw' / 'ratings.csv'
        if feather_path.exists():
            try:
                df = pd.read_feather(feather_path, columns=['userId', 'movieId', 'rating'])
            except Exception:
                df = pd.read_feather(feather_path)
        elif pkl_path.exists():
            df = pd.read_pickle(pkl_path)
        elif raw_csv_path.exists():
            # 读取原始 CSV（列：userId,movieId,rating,timestamp）
            try:
                df = pd.read_csv(raw_csv_path, usecols=['userId', 'movieId', 'rating'])
            except Exception:
                df = pd.read_csv(raw_csv_path)
        if df is None or df.empty:
            return []
        # 列名兼容
        uid_col = 'userId' if 'userId' in df.columns else 'user_id' if 'user_id' in df.columns else None
        mid_col = 'movieId' if 'movieId' in df.columns else 'movie_id' if 'movie_id' in df.columns else None
        rating_col = 'rating' if 'rating' in df.columns else ('score' if 'score' in df.columns else None)
        if not uid_col or not mid_col or not rating_col:
            return []
        # 过滤用户
        try:
            df[uid_col] = pd.to_numeric(df[uid_col], errors='coerce')
        except Exception:
            pass
        sub = df[df[uid_col] == int(user_id)][[mid_col, rating_col]].copy()
        if sub.empty:
            return []
        # 排序：分数高优先，再次序稳定
        try:
            sub = sub.sort_values(by=[rating_col, mid_col], ascending=[False, True])
        except Exception:
            pass
        # 限制数量
        if limit and limit > 0:
            sub = sub.head(int(limit))
        items: list[dict] = []
        for _, row in sub.iterrows():
            try:
                mid = int(row[mid_col]) if pd.notna(row[mid_col]) else None
            except Exception:
                mid = None
            try:
                sc = float(row[rating_col]) if pd.notna(row[rating_col]) else 0.0
            except Exception:
                sc = 0.0
            if mid is None:
                continue
            items.append({'movieId': mid, 'score': sc})
        return items
    except Exception:
        return []


@router.get("/user/ratings")
def get_user_ratings(token: str = Query(...), limit: int = Query(500, ge=1, le=5000), db: Session = Depends(get_db)):
    """返回用户的评分列表，并附带电影基础信息（海报、标题、年份、类型）。

    输出示例：
    [ { id, movieId, title, year, genres, poster, score } ]
    """
    user = get_current_user(token, db)
    # 先取数据库评分，按最新(id 倒序) 排序
    ratings = (
        db.query(Rating)
        .filter(Rating.user_id == user.id)
        .order_by(Rating.id.desc())
        .limit(limit)
        .all()
    )

    base_items: list[dict] = []
    movie_ids: list[int] = []

    if ratings:
        for r in ratings:
            try:
                mid = int(r.movie_id)
            except Exception:
                continue
            movie_ids.append(mid)
            base_items.append({'id': mid, 'movieId': mid, 'score': float(r.score) if r.score is not None else 0.0})
    # 若不足 limit，用离线历史评分补齐，避免“只有最近评分”的观感
    if len(base_items) < limit:
        fb = _fallback_user_ratings_from_processed(int(user.id), limit=limit)
        for it in fb:
            if len(base_items) >= limit:
                break
            try:
                mid = int(it.get('movieId') or it.get('id'))
            except Exception:
                continue
            if mid in movie_ids:
                continue
            movie_ids.append(mid)
            base_items.append({'id': mid, 'movieId': mid, 'score': float(it.get('score') or 0.0)})

    if not base_items:
        return []

    # 从 movies_cleaned 中补充 title/year/genres
    if _movies_meta_df is not None and not _movies_meta_df.empty:
        try:
            sub = _movies_meta_df[_movies_meta_df['movieId'].isin(movie_ids)][['movieId', 'title', 'year', 'genres']]
            meta_map = { int(row.movieId): row for _, row in sub.iterrows() }
            for it in base_items:
                row = meta_map.get(int(it['movieId']))
                if row is not None:
                    it.setdefault('title', row.get('title'))
                    it.setdefault('year', row.get('year'))
                    it.setdefault('genres', row.get('genres'))
        except Exception:
            pass

    enriched = _enrich_with_tmdb(base_items)
    out: list[dict] = []
    for it in enriched:
        out.append({
            'id': it.get('movieId') or it.get('id'),
            'movieId': it.get('movieId') or it.get('id'),
            'title': it.get('title'),
            'genres': it.get('genres'),
            'year': it.get('year'),
            'poster': it.get('poster'),
            'score': it.get('score'),
        })
    return _json_safe(out)


@router.get("/movie/{movie_id}/ratings")
def get_movie_ratings(movie_id: int, limit: int = Query(10, ge=1, le=100), token: str | None = Query(None), request: Request = None, db: Session = Depends(get_db)):
    """返回指定电影的其他用户评分列表与统计信息。
    输出：
      {
        "movie_id": 1,
        "average": 4.3,
        "count": 12,
        "items": [ {"username": "alice", "score": 5}, ... ]
      }
    """
    # 统一使用规范化后的 movieId（兼容传入 tmdbId 的情况）
    normalized_mid = _normalize_movie_id(movie_id)

    # 解析当前用户（用于排除自己的评分明细）
    my_user_id: int | None = None
    try:
        # 若未通过 query 提供 token，则尝试从 Authorization 头解析
        if token is None and request is not None:
            try:
                auth = request.headers.get('Authorization') or request.headers.get('authorization')
                if auth and auth.lower().startswith('bearer '):
                    token = auth.split(' ', 1)[1].strip()
            except Exception:
                pass
        if token:
            try:
                user = get_current_user(token, db)
                my_user_id = int(user.id)
            except Exception:
                my_user_id = None
    except Exception:
        my_user_id = None

    # 统计平均分与总数（应用数据库）
    avg_count = db.query(func.avg(Rating.score), func.count(Rating.id)) \
        .filter(Rating.movie_id == normalized_mid).first()
    avg_score = float(avg_count[0]) if avg_count and avg_count[0] is not None else 0.0
    total = int(avg_count[1]) if avg_count and avg_count[1] is not None else 0

    # 最近的若干条评分，附带用户名
    q = (
        db.query(Rating.score, User.username, User.id)
        .join(User, User.id == Rating.user_id)
        .filter(Rating.movie_id == normalized_mid)
    )
    # 排除当前用户的评分明细，确保前端“其他用户评分”不包含自己
    if my_user_id is not None:
        try:
            q = q.filter(Rating.user_id != my_user_id)
        except Exception:
            pass
    rows = q.order_by(Rating.id.desc()).limit(limit).all()
    items = [{"username": (u or "匿名用户"), "score": float(s)} for (s, u, _uid) in rows]

    # 若数据库暂无评分记录，优先回退到 movies_cleaned.pkl 中的全量统计
    if total == 0 and _movies_meta_df is not None:
        try:
            sub = _movies_meta_df[_movies_meta_df['movieId'] == int(normalized_mid)]
            if not sub.empty:
                rm = sub.iloc[0].get('rating_mean')
                rc = sub.iloc[0].get('rating_count')
                if rm is not None:
                    try:
                        avg_score = float(rm)
                    except Exception:
                        avg_score = 0.0
                if rc is not None:
                    try:
                        total = int(rc)
                    except Exception:
                        total = 0
        except Exception:
            pass

    # 仍无统计则退回到冷启动 popular_movies（可能是子集，不保证全量）
    if total == 0 and cold_start_recommender is not None:
        try:
            df = getattr(cold_start_recommender, 'popular_movies', None)
            if df is not None and len(df) > 0:
                row = df[df['movieId'] == int(normalized_mid)]
                if not row.empty:
                    rm = row.iloc[0].get('rating_mean')
                    rc = row.iloc[0].get('rating_count')
                    if rm is not None:
                        try:
                            avg_score = float(rm)
                        except Exception:
                            avg_score = 0.0
                    if rc is not None:
                        try:
                            total = int(rc)
                        except Exception:
                            total = 0
        except Exception:
            pass

    # 若明细为空，尝试从 processed 评分数据中抽取匿名明细返回，增强前端可视性
    if not items:
        fb = _fallback_ratings_items_from_processed(normalized_mid, limit=limit)
        if fb:
            # 同样排除“我自己”的匿名条目（离线明细用户名为：用户{uid}）
            if my_user_id is not None:
                my_name = f"用户{my_user_id}"
                items = [it for it in fb if str(it.get('username') or '') != my_name]
            else:
                items = fb

    # 解析当前用户评分（如果提供了 token 或 Authorization）
    my_rating_val = None
    try:
        if my_user_id is not None:
            r = db.query(Rating.score).filter(Rating.user_id == my_user_id, Rating.movie_id == normalized_mid).first()
            if r and r[0] is not None:
                my_rating_val = float(r[0])
    except Exception:
        my_rating_val = None

    return _json_safe({
        "movie_id": int(normalized_mid),
        "average": round(avg_score, 2),
        "count": total,
        "items": items,
        "my_rating": my_rating_val
    })


@router.get("/stream/{movie_id}")
def stream_movie(movie_id: int, request: Request):
    """按需分段流式返回 mp4 内容，支持 Range 请求。视频文件需放置在 data/videos/{movie_id}.mp4。"""
    file_path = VIDEO_DIR / f"{movie_id}.mp4"
    if not file_path.exists():
        raise HTTPException(status_code=404, detail="Video not found")

    file_size = file_path.stat().st_size
    range_header = request.headers.get('range') or request.headers.get('Range')
    start = 0
    end = file_size - 1

    if range_header:
        # 解析形如: bytes=start-end
        try:
            units, _range = range_header.split('=')
            if units.strip().lower() != 'bytes':
                raise ValueError('Invalid units')
            start_str, end_str = _range.split('-')
            if start_str:
                start = int(start_str)
            if end_str:
                end = int(end_str)
        except Exception:
            # 不合法 Range 时返回 416
            raise HTTPException(status_code=416, detail="Invalid Range header")

        start = max(0, start)
        end = min(end, file_size - 1)
        if start > end:
            raise HTTPException(status_code=416, detail="Requested Range Not Satisfiable")

        headers = {
            'Content-Range': f'bytes {start}-{end}/{file_size}',
            'Accept-Ranges': 'bytes',
            'Content-Length': str(end - start + 1),
            'Content-Type': 'video/mp4'
        }
        return StreamingResponse(_range_stream(file_path, start, end), status_code=206, headers=headers)

    # 无 Range，完整返回
    headers = {
        'Accept-Ranges': 'bytes',
        'Content-Length': str(file_size),
        'Content-Type': 'video/mp4'
    }
    return StreamingResponse(_range_stream(file_path, 0, end), status_code=200, headers=headers)


def _find_trailer_file(movie_id: int) -> Path | None:
    """尝试定位预告片文件，优先使用 movieId，其次使用 tmdbId。
    约定目录：data/videos/trailers/{id}.mp4
    """
    # movieId
    p1 = VIDEO_DIR / "trailers" / f"{movie_id}.mp4"
    if p1.exists():
        return p1
    # tmdbId
    try:
        tmdb_id = _movie_to_tmdb.get(int(movie_id)) if _movie_to_tmdb else None
        if tmdb_id:
            p2 = VIDEO_DIR / "trailers" / f"{tmdb_id}.mp4"
            if p2.exists():
                return p2
    except Exception:
        pass
    return None


@router.get("/trailer/{movie_id}")
def stream_trailer(movie_id: int, request: Request):
    """分段流式返回预告片 mp4 内容。查找路径 data/videos/trailers/{movie_id}.mp4 或 {tmdbId}.mp4。"""
    file_path = _find_trailer_file(movie_id)
    if not file_path:
        raise HTTPException(status_code=404, detail="Trailer not found")

    file_size = file_path.stat().st_size
    range_header = request.headers.get('range') or request.headers.get('Range')
    start = 0
    end = file_size - 1

    if range_header:
        try:
            units, _range = range_header.split('=')
            if units.strip().lower() != 'bytes':
                raise ValueError('Invalid units')
            start_str, end_str = _range.split('-')
            if start_str:
                start = int(start_str)
            if end_str:
                end = int(end_str)
        except Exception:
            raise HTTPException(status_code=416, detail="Invalid Range header")

        start = max(0, start)
        end = min(end, file_size - 1)
        if start > end:
            raise HTTPException(status_code=416, detail="Requested Range Not Satisfiable")

        headers = {
            'Content-Range': f'bytes {start}-{end}/{file_size}',
            'Accept-Ranges': 'bytes',
            'Content-Length': str(end - start + 1),
            'Content-Type': 'video/mp4'
        }
        return StreamingResponse(_range_stream(file_path, start, end), status_code=206, headers=headers)

    headers = {
        'Accept-Ranges': 'bytes',
        'Content-Length': str(file_size),
        'Content-Type': 'video/mp4'
    }
    return StreamingResponse(_range_stream(file_path, 0, end), status_code=200, headers=headers)


@router.get("/movie/{movie_id}/videos")
def get_movie_videos(movie_id: int):
    """返回可用的视频资源：
    - 本地正片/预告片（可 Range 直播）
    - TMDB 视频（优先 YouTube 预告片），用于无本地预告片时的在线播放

    输出示例：
    {
      has_stream: bool,
      stream: "/recommend/stream/{id}" | null,
      has_trailer: bool,
      trailer: "/recommend/trailer/{id}" | null,
      tmdb_trailers: [ { site, key, type, name, url, embed_url } ]
    }
    """
    stream_path = VIDEO_DIR / f"{movie_id}.mp4"
    has_stream = stream_path.exists()
    has_trailer = _find_trailer_file(movie_id) is not None
    base = "/recommend"

    # 解析 TMDB JSON 中的视频字段
    tmdb_list: list[dict] = []
    tmdb_id = _movie_to_tmdb.get(int(movie_id)) if _movie_to_tmdb else None
    if tmdb_id:
        data = _load_tmdb_json(int(tmdb_id))
        if isinstance(data, dict):
            videos = data.get('videos')
            # 1) 标准列表格式
            if isinstance(videos, list):
                for v in videos:
                    if not isinstance(v, dict):
                        continue
                    site = (v.get('site') or '').strip()
                    vtype = (v.get('type') or '').strip()
                    key = (v.get('key') or '').strip()
                    name = (v.get('name') or '').strip()
                    official = bool(v.get('official')) if 'official' in v else None
                    if site and key:
                        url = None
                        embed = None
                        if site.lower() == 'youtube':
                            url = f"https://www.youtube.com/watch?v={key}"
                            embed = f"https://www.youtube.com/embed/{key}"
                        tmdb_list.append({
                            'site': site,
                            'type': vtype or None,
                            'key': key,
                            'name': name or None,
                            'official': official,
                            'url': url,
                            'embed_url': embed,
                        })
            # 2) 兼容单 key 场景（如 trailer_key / youtube_key / trailer）
            for k in ('trailer_key', 'youtube_key', 'trailer'):
                if k in data and isinstance(data.get(k), str) and data.get(k).strip():
                    key = data.get(k).strip()
                    tmdb_list.append({
                        'site': 'YouTube',
                        'type': 'Trailer',
                        'key': key,
                        'name': 'Trailer',
                        'official': None,
                        'url': f"https://www.youtube.com/watch?v={key}",
                        'embed_url': f"https://www.youtube.com/embed/{key}",
                    })

    return _json_safe({
        "has_stream": has_stream,
        "stream": f"{base}/stream/{movie_id}" if has_stream else None,
        "has_trailer": has_trailer,
        "trailer": f"{base}/trailer/{movie_id}" if has_trailer else None,
        "tmdb_trailers": tmdb_list,
    })